#!/usr/bin/python -u
'''Brultech ECM-1240 Data POSTer.
    This code was written and modified by the following:
    Amit Snyderman <amit@amitsnyderman.com>
    bpwwer & tenholde from the cocoontech.com forums
    Kelvin Kakugawa
    brian jackson [http://fivejacksons.com/brian/]
    Marc MERLIN <marc_soft@merlins.org> | http://marc.merlins.org/perso/solar/
    Brad Norman <brad@greenhomegear.com>
    
    The code receives binary energy packets from a Brultech ECM-1240 via
    a TCP/IP socket, decodes the data, opens TCP/IP socket and POSTs the data to
    an online energy dashboard and analytics company.
    
    The code needs to be modified to differentiate data from several ECMs.
    ECM data needs to be stored based on the "unit-id" for both "now" and
    "prev" data.  Intuitively it appears I need help modifying classes,
    MovingBuffer and BaseProcessor.  25 years ago I understood procedural
    code.  OOP is new to me.  I wish to understand this code.
'''

import pycurl
import bisect
import new
import socket
import sys
import time
import traceback

# DEFINE CONSTANTS
MINUTE  = 60
HOUR    = 60 * MINUTE
DAY     = 24 * HOUR

# PACKET SETTINGS
START_HEADER0    = hex(254)
START_HEADER1    = hex(255)
START_HEADER2    = hex(3)

DATA_BYTES_LENGTH = 62
SEC_COUNTER_MAX   = 16777216

# ETHERNET SETTINGS
HOST = ''
PORT = 8083        # default port that Brultech EngineG is pushing data to

# Helper Functions

def calculate(now, prev):
    '''calc average watts/s between packets'''
    if now['secs'] < prev['secs']:
        now['secs'] += SEC_COUNTER_MAX # handle seconds counter overflow
    secs_delta = float(now['secs'] - prev['secs'])
    
    # CH1/2 Watts
    now['ch1_watts'] = (now['ch1_aws'] - prev['ch1_aws']) / secs_delta
    now['ch2_watts'] = (now['ch2_aws'] - prev['ch2_aws']) / secs_delta

    now['ch1_positive_watts'] = (now['ch1_pws'] - prev['ch1_pws']) / secs_delta
    now['ch2_positive_watts'] = (now['ch2_pws'] - prev['ch2_pws']) / secs_delta

    now['ch1_negative_watts'] = now['ch1_watts'] - now['ch1_positive_watts']
    now['ch2_negative_watts'] = now['ch2_watts'] - now['ch2_positive_watts']

    # All Watts increase no matter which way the current is going
    # Polar Watts only increase if the current is positive
    # Every Polar Watt does register as an All Watt too.
    # math comes to: Watts = 2x Polar Watts - All Watts
    now['ch1_pwh']  = now['ch1_pws'] / 3600000.0
    now['ch2_pwh']  = now['ch2_pws'] / 3600000.0
    now['ch1_nwh']  = (now['ch1_aws'] - now['ch1_pws']) / 3600000.0
    now['ch2_nwh']  = (now['ch2_aws'] - now['ch2_pws']) / 3600000.0
    now['ch1_wh']   = now['ch1_nwh'] - now['ch1_pwh']
    now['ch2_wh']   = now['ch2_pwh'] - now['ch2_nwh']


    now['aux1_wh'] = now['aux1_ws'] / 3600000.0
    now['aux2_wh'] = now['aux2_ws'] / 3600000.0
    now['aux3_wh'] = now['aux3_ws'] / 3600000.0
    now['aux4_wh'] = now['aux4_ws'] / 3600000.0
    now['aux5_wh'] = now['aux5_ws'] / 3600000.0


    # Polar Watts' instant value's only purpose seems to help find out if
    # main watts are positive or negative. Polar Watts only goes up if the
    # sign is positive. If they are null, that means the sign is negative.
    if (now['ch1_positive_watts'] == 0):
        now['ch1_watts'] *= -1 
    if (now['ch2_positive_watts'] == 0):
        now['ch2_watts'] *= -1 

    # AUX1-5 Watts
    now['aux1_watts'] = (now['aux1_ws'] - prev['aux1_ws']) / secs_delta
    now['aux2_watts'] = (now['aux2_ws'] - prev['aux2_ws']) / secs_delta
    now['aux3_watts'] = (now['aux3_ws'] - prev['aux3_ws']) / secs_delta
    now['aux4_watts'] = (now['aux4_ws'] - prev['aux4_ws']) / secs_delta
    now['aux5_watts'] = (now['aux5_ws'] - prev['aux5_ws']) / secs_delta

    now['time'] = time.strftime("%Y/%m/%d %H:%M:%S", time.localtime())

# Packet Server Classes

class BasePacketServer(object):

    def __init__(self, packet_processor):
        self.packet_processor = packet_processor

    def _convert(self, bytes):
        ''' convert binary watt/second counters to integers '''
        return reduce(lambda x,y:x+y[0] * (256**y[1]), zip(bytes,xrange(len(bytes))),0)

    def _compile(self, packet):
        ''' decode the 57 byte data packet '''
        now = {}

        # Voltage Data (2 bytes)
        now['volts'] = 0.1 * self._convert(packet[1::-1])

        # CH1-2 Absolute Watt-Second Counter (5 bytes each)
        now['ch1_aws'] = self._convert(packet[2:7])
        now['ch2_aws'] = self._convert(packet[7:12])

        # CH1-2 Polarized Watt-Second Counter (5 bytes each)
        now['ch1_pws'] = self._convert(packet[12:17])
        now['ch2_pws'] = self._convert(packet[17:22])

        # Reserved (4 bytes)

        # Device Serial Number (2 bytes)
        now['ser_no'] = self._convert(packet[27:25:-1])

        # Reset and Polarity Information (1 byte)
        now['flag'] = self._convert(packet[28:29])

        # Device Information (1 byte)
        now['unit_id'] = self._convert(packet[29:30])

        # CH1-2 Current (2 bytes each)
        now['ch1_amps'] = 0.01 * self._convert(packet[30:32])
        now['ch2_amps'] = 0.01 * self._convert(packet[32:34])

        # Seconds (3 bytes)
        now['secs'] = self._convert(packet[34:37])

        # AUX1-5 Watt-Second Counter (4 bytes each)
        now['aux1_ws'] = self._convert(packet[37:41])
        now['aux2_ws'] = self._convert(packet[41:45])
        now['aux3_ws'] = self._convert(packet[45:49])
        now['aux4_ws'] = self._convert(packet[49:53])
        now['aux5_ws'] = self._convert(packet[53:57])

        return now

    def process(self, packet):
        packet = self._compile(packet)

        self.packet_processor.preprocess(packet)
        self.packet_processor.process(packet)

    def read(self):
        pass
    
    def run(self):
        ''' main loop to read TCP/IP packets '''
        try:
            self.packet_processor.setup()

            while True:
                try:
                    self.read()

                except Exception, e:
                    if type(e) == KeyboardInterrupt: # only break for KeyboardInterrupt
                        raise e

                    traceback.print_exc()
                    if not self.packet_processor.handle(e):
                        print 'Exception [in %s]: %s' % (self, e)

        except Exception, e:
            traceback.print_exc()
            sys.exit(1)

        finally:
            self.packet_processor.cleanup()

class SocketPacketServer(BasePacketServer):
    def __init__(self, packet_processor, host=HOST, port=PORT):
        super(SocketPacketServer, self).__init__(packet_processor)

        socket.setdefaulttimeout(60) # override None

        self._host = host
        self._port = port

        self.sock = None
        self.conn = None

    def read(self):
        ''' open TCP/IP client socket to ECM-1240 data server '''
        try:
            self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

            self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            try:
                self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
            except: # REUSEPORT may not be supported on all systems
                pass

            self.sock.bind((self._host, self._port))
            self.sock.listen(1)

            self.conn, addr = self.sock.accept()

            while True:
                data = self.conn.recv(1)
                if not data:
                    break

                # determine start of new packet
                header_byte0 = hex(ord(data))
                if header_byte0 != START_HEADER0:
                    continue

                data = self.conn.recv(1)
                header_byte1 = hex(ord(data))
                if header_byte1 != START_HEADER1:
                    continue

                data = self.conn.recv(1)
                header_byte2 = hex(ord(data))
                if header_byte2 != START_HEADER2:
                    continue

                # gather packet data
                packet = self.conn.recv(DATA_BYTES_LENGTH)
                while len(packet) < DATA_BYTES_LENGTH:
                    data = self.conn.recv(DATA_BYTES_LENGTH-len(packet))
                    if not data: # No data left
                        raise Exception('Received no data.')
                    packet += data
                packet = [ord(c) for c in packet]
                # *** THIS IS WHERE THE CODE NEEDS TO BE MODIFIED
                # packet[29:30] is the "unit id" which indicates 
                # which ECM unit you are receiving data from.
                # When calling self.process, a unit id needs to be
                # passed also.
                if packet[29:30] == [3]:    # blocks packets from other ECMs
                    continue                # added by bn

                self.process(packet)

        finally:
            if self.conn:
                self.conn.shutdown(socket.SHUT_RD)
                self.conn.close()
                self.conn = None

            if self.sock:
                self.sock.shutdown(socket.SHUT_RD)
                self.sock.close()
                self.sock = None

# Packet Processor Classes

class MovingBuffer(object):
    '''Maintain fixed-size buffer of data over time'''
    def __init__(self, max_timeframe=DAY):
        self.time_points    = []
        self.max_timeframe    = max_timeframe

    def insert(self, timestamp, time_dict):
        bisect.insort(self.time_points, (timestamp, time_dict))

        now = int(time.time())
        cull_index = bisect.bisect(self.time_points, (now-self.max_timeframe, {}))
        del(self.time_points[:cull_index])

    def data_over(self, time_delta):
        now = int(time.time())
        delta_index = bisect.bisect(self.time_points, (now-time_delta, {}))

        return self.time_points[delta_index:]

    def delta_over(self, time_delta):
        now = int(time.time())
        delta_index = bisect.bisect(self.time_points, (now-time_delta, {}))

        offset = self.time_points[delta_index][1]
        current = self.time_points[-1][1]

        calculate(current, offset)
        return current

class BaseProcessor(object):
    def __init__(self, buffer_timeframe, *args, **kwargs):
        self.buffer    = MovingBuffer(buffer_timeframe)

    def setup(self):
        pass

    def preprocess(self, packet):
        self.buffer.insert(int(time.time()), packet)

    def process(self, packet):
        pass
        
    def post(self):     # added by bn
        pass            # added by bn

    def handle(self, exception):
        return False

    def cleanup(self):
        pass

class PrintMixin(object):
    def __init__(self, *args, **kwargs):
        super(PrintMixin, self).__init__(*args, **kwargs)

        self.print_out = kwargs.get('print_out', False)
        self.prev_packet = {}

    def process(self, packet):
        ''' format data payload and POST using cURL 
            this section was completely changed by bn '''
        if self.prev_packet:
            calculate(packet, self.prev_packet)
            
            # payload format is: "meter-id", "kWh", "timestamp"
            epoch = "%.0f" % (time.time())
            payload = "654," + "%1.4f," % (packet['ch1_watts']/1000) + epoch +\
            ",655," + "%1.4f," % (packet['ch2_negative_watts']/1000) + epoch +\
            ",656," + "%1.4f," % (packet['aux1_watts']/1000) + epoch +\
            ",657," + "%1.4f," % (packet['aux2_watts']/1000) + epoch +\
            ",658," + "%1.4f," % (packet['aux3_watts']/1000) + epoch +\
            ",659," + "%1.4f," % (packet['aux4_watts']/1000) + epoch +\
            ",660," + "%1.4f," % (packet['aux5_watts']/1000) + epoch
            
            print
            print "====== ECM-1240 Payload Data ======"
            print payload
            print "==================================="
            print
            
            # POST payload to PlotWatt using PycURL
            c = pycurl.Curl()
            c.setopt(c.URL, 'http://insert-HouseCode-here:@plotwatt.com/api/v2/push_readings')
            c.setopt(c.POSTFIELDS, payload)
            c.setopt(c.VERBOSE, 1)
            c.perform()
            c.close()
            
        # *** THIS IS WHERE CODE NEEDS TO BE MODIFIED
        # packet needs to be "unit id" differentiated
        self.prev_packet = packet
        super(PrintMixin, self).process(packet)

if __name__ == '__main__':
    buffer_timeframe = 5*MINUTE
    bases = [BaseProcessor]
    bases.insert(0, PrintMixin)
    Processor = new.classobj('Processor', tuple(bases), {})
    processor = Processor(buffer_timeframe)
    server = SocketPacketServer(processor)
    server.run()
    sys.exit(0)
